渲染机制

# 渲染机制

[TOC]

# 一、DOCTYPE及作用

# 1.1 DTD(Document Type Definition):文档类型定义。

是一系列的语法规则,用来定义XML或者(X)HTML文件类型。浏览器会使用DTD来判断文本类型,决定使用何种协议来解析,以及切换浏览器模式。(说白了就是:DTD就是告诉浏览器,我是什么文档类型,你要用什么协议来解析我。)

DOCTYPE:用来声明DTD规范。

一个主要的用途便是文件的合法性验证。如果文件代码不合法,那么浏览器解析时便会出现一些差错。(说白了,DOCTYPE就是用来声明DTD的)

# 1.2 HTML5的写法

<!DOCTYPE html>
1

# 二、浏览器渲染过程

浏览器渲染

# 2.1 基本概念

从HTTP请求回来,就产生了流式的数据,后续的DOM树构建、CSS计算、渲染、合成、绘制,都是尽可能地流式处理前一步的产出:即不需要等到上一步骤完全结束,就开始处理上一步的输出,这样在浏览网页时,才会看到逐步出现的页面。

  • DOM Tree:浏览器将HTML解析成树形的数据结构(DOM 树)。
  • CSS Rule Tree:浏览器将CSS解析成树形的数据结构。
  • Render Tree: DOM和CSSOM合并后生成Render Tree。(虽然有了Render Tree,但并不知道节点的位置,需要依靠接下来的layout)
  • layout: 有了Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的CSS定义以及他们的从属关系,从而去计算出每个节点在屏幕中的位置(宽高、颜色等)。
  • painting:按照算出来的规则,通过显卡,把内容画到屏幕上。
  • display:打击看到的最终效果。

# 2.2 重排reflow

# 2.2.1 定义

部分渲染树(或者整个渲染树)需要重新分析并且节点尺寸需要重新计算,表现为重新生成布局,重新排列元素。

# 2.2.2 触发reflow

根据Render Tree布局(几何属性),意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树。

  • 增加、删除、修改DOM节点时,会导致 Reflow 或 Repaint。

  • 移动DOM的位置

  • 修改CSS样式时(宽高、display 为none等,都是通过css样式来修改的)

  • 当用户Resize窗口时(移动端没有这个问题),或是滚动的时候,有可能会触发(具体要看浏览器的规则)。

  • 修改网页的默认字体时(这个影响非常严重)。

  • 现代浏览器会对重排做优化,它会等到足够数量的变化发生,再做一次批处理重排。

    • 页面第一次渲染(初始化)
    • DOM树变化(如:增删节点)
    • Render树变化(如:padding改变)
    • 浏览器窗口resize
    • 获取元素的某些属性:
  • 浏览器为了获得正确的值也会提前触发重排,这样就使得浏览器的优化失效了,这些属性包括offsetLeftoffsetTopoffsetWidthoffsetHeightscrollTop/Left/Width/HeightclientTop/Left/Width/Height、调用了getComputedStyle()或者IE的currentStyle

# 2.3.2 避免reflow

reflow重排的成本开销要高于repaint重绘,一个节点的重排往往会导致子节点以及同级节点的重排。

# 2.3.2.1 使用DocumentFragment

可以通过createDocumentFragment创建一个游离于DOM树之外的节点,然后在此节点上批量操作,最后插入DOM树中,因此只触发一次重排。

var fragment = document.createDocumentFragment();

for (let i = 0;i<10;i++){
  let node = document.createElement("p");
  node.innerHTML = i;
  fragment.appendChild(node);
}

document.body.appendChild(fragment);
1
2
3
4
5
6
7
8
9

# 2.3 重绘repaint

# 2.3.1 定义

由于节点的几何属性发生改变或者由于样式发生改变,例如改变元素背景色时,屏幕上的部分内容需要更新,表现为某些元素的外观被改变。

# 2.3.2 触发repaint

  • DOM改动
  • CSS改动

其实,就是判断当前呈现的内容是否发生变化(无论这个变化是通过DOM改动还是CSS改动)。只要页面显示的内容不一样了,肯定要 Repaint。

意味着元素发生的改变只影响了节点的一些样式(背景色,边框颜色,文字颜色等),只需要应用新样式绘制这个元素就可以了。

重绘不一定重排,重排必然引起重绘。

  • 背景色、颜色、字体改变(注意:字体大小发生变化时,会触发重排)

# 2.3.3 减少repaint

(1)如果需要创建多个DOM节点,可以使用DocumentFragment创建完,然后一次性地加入document。(加一个节点,就repaint一次,不太好)

(2)将元素的display设置为”none”,完成修改后再把display修改为原来的值。

小知识

实际上,“绘制”发生的频率比想象中要高得多。考虑一个情况:鼠标划过浏览器显示区域。这个过程中,鼠标的每次移动,都造成了重新绘制,如果不重新绘制,就会产生大量的鼠标残影。

计算机图形学中,使用的方案就是**“脏矩形”**算法,也就是把屏幕均匀地分成若干矩形区域。

当鼠标移动、元素移动或者其它导致需要重绘的场景发生时,只重新绘制它所影响到的几个矩形区域就够了。比矩形区域更小的影响最多只会涉及 4 个矩形,大型元素则覆盖多个矩形。

设置合适的矩形区域大小,可以很好地控制绘制时的消耗。设置过大的矩形会造成绘制面积增大,而设置过小的矩形则会造成计算复杂。

重新绘制脏矩形区域时,把所有与矩形区域有交集的合成层(位图)的交集部分绘制即可。

# 2.4 浏览器渲染过程

# 2.4.1 解析HTML,生成DOM树(这里遇到外链,此时会发起请求)

  • 无论是DOM还是CSSOM,都是要经过 Bytes→characters→tokens→nodes→objectmodel这个过程。
  • DOM树构建过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。
# 2.4.1.1 构建DOM树

dom-tree

  1. 转码:浏览器将接收的二进制数据按照指定编码格式化为HTML字符串。
  2. 生成Tokens:浏览器将HTML字符串解析成Tokens。
  3. 构建Nodes:对Node添加特定的属性,通过指针确定Node的父、子、兄弟关系和所属的treeScope。
  4. 生成DOM树:通过node包含的指针确定的关系构建出DOM树。
  • 具体过程如下:

要把这些简单的词变成 DOM 树,这个过程是使用来实现的。

// HTML 的语法分析器
function HTMLSyntaticalParser(){
    var stack = [new HTMLDocument];
    // receiveInput 负责接收词法部分产生的词(token)
    // 通常可以由 emmitToken 来调用
    this.receiveInput = function(token) {
        // 构建 DOM 树
    }
    this.getOutput = function(){
        return stack[0];
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

为了构建 DOM 树,需要一个 Node 类,接下来所有的节点都会是这个 Node 类的实例。

暂时把 Node 分为 Element 和 Text。

function Element(){
    this.childNodes = [];
}
function Text(value){
    this.value = value || "";
}
1
2
3
4
5
6

前面词(token)中,tag start、tag end是需要成对匹配的。

根据一些编译原理中常见的技巧,使用的栈正是用于匹配开始和结束标签的方案。

对于 Text 节点,则需要把相邻的 Text 节点合并起来,的做法是当词(token)入栈时,检查栈顶是否是 Text 节点,如果是的话就合并 Text 节点。

通过这个栈,可以构建 DOM 树:

  • 栈顶元素就是当前节点;
  • 遇到属性,就添加到当前节点;
  • 遇到文本节点,如果当前节点是文本节点,则跟文本节点合并,否则入栈成为当前节点的子节点;
  • 遇到注释节点,作为当前节点的子节点;
  • 遇到 tag start 就入栈一个节点,当前节点就是这个节点的父节点;
  • 遇到 tag end 就出栈一个节点(还可以检查是否匹配)。

# 2.4.2 解析CSS,生成CSSOM树(CSS规则树)

  • 在最终计算各个节点的样式时,浏览器都会先从该节点的普遍属性(比如body里设置的全局样式)开始,再去应用该节点的具体属性。
  • 每个浏览器都有自己默认的样式表,因此很多时候这棵CSSOM树只是对这张默认样式表的部分替换。
# 2.4.2.1 CSS选择器解析

参考资料:浏览器如何解析css选择器 (opens new window)

  • 浏览器会从右往左解析CSS选择器。
    • 因为从右往左的匹配在第一步就筛掉了大量的不符合条件的叶子节点,而从左往右的匹配会把性能浪费在失败的查找上。
    • 若从左向右的匹配,发现不符合规则,需要进行回溯,会损失很多性能。

# 2.4.3 合并DOM树和CSSOM树,生成render树

生成render树

  • DOM树从根节点开始遍历可见节点,这里之所以强调了“可见”,是因为如果遇到设置了类似 display:none;的不可见节点,在render过程中是会被跳过的(但 visibility:hidden;opacity:0这种仍旧占据空间的节点不会被跳过render),保存各个节点的样式信息及其余节点的从属关系。

# 2.4.4 布局render树(Layout/reflow),计算各元素尺寸、位置

  • 有了各个节点的样式信息和属性,但不知道各个节点的确切位置和大小,所以要通过布局将样式信息和属性转换为实际可视窗口的相对大小和位置。

# 2.4.5 绘制render树(paint),绘制页面像素信息

  • 最后只要将确定好位置大小的各节点,通过GPU渲染到屏幕的实际像素。

# 2.4.6 浏览器会将各层的信息发送给GPU,GPU将各层合成(composite),显示在屏幕上

  • GPU(Graphics Processing Unit)图形处理器

# 2.4.7 注意事项

  • 在上述渲染过程中,前3点可能要多次执行,比如js脚本去操作dom、更改css样式时,浏览器又要重新构建DOM、CSSOM树,重新render,重新layout、paint。
  • Layout在Paint之前,因此每次Layout重新布局(reflow 重排)后都要重新出发Paint渲染,这时又要去消耗GPU。
  • Paint不一定会触发Layout,比如改个颜色改个背景;(repaint 重绘)
  • 图片下载完也会重新出发Layout和Paint。
  • GoogleChromeLabs 里面有一个csstriggers,列出了各个CSS属性对浏览器执行Layout、Paint、Composite的影响。

# 2.5 首屏优化

  • 减少资源请求数量(内联亦或是延迟动态加载)。

  • 使CSS样式表尽早加载,减少@import的使用,因为需要解析完样式表中所有import的资源才会算CSS资源下载完。

  • 异步js:阻塞解析器的 JavaScript 会强制浏览器等待 CSSOM 并暂停 DOM 的构建,导致首次渲染的时间延迟。 js阻塞浏览器解析

  • 避免逐个修改节点样式,尽量一次性修改

  • 使用DocumentFragment将需要多次修改的DOM元素缓存,最后一次性append到真实DOM中渲染

  • 可以将需要多次修改的DOM元素设置 display:none,操作完再显示。(因为隐藏元素不在render树内,因此修改隐藏元素不会触发回流重绘)

  • 避免多次读取某些属性(见上)

  • 将复杂的节点元素脱离文档流,降低回流成本

操作DOM具体的成本,说到底是造成浏览器回流reflow和重绘reflow,从而消耗GPU资源。

# 2.6 布局Layout

# 三、不同CSS样式渲染区别

  • GUI渲染页面时,当遇到其他请求时的两种处理方法:
    • 不开辟线程:等待CSS文件加载完才继续往下渲染。
    • 开辟线程去加载CSS文件,继续渲染。
    • 故用link相比@import,页面渲染速度更快。
  • 开辟HTTP请求线程,专门加载CSS文件。

# 3.2 @import导入

  • 不会开辟新线程。

# 3.3 内联样式

  • 不需要额外的HTTP请求。
  • HTML加载的同时也加载了CSS。